#!/usr/bin/env python
# DeskBling by Jason Gorski (jgorski@gmail.com)
#  uses some snippets from WallpaperClockScreenlet by Whise aka Helder Fraga
#
#Requires: python-imaging, gtk, gnome (gconf)
#
# TODO get rid of reliance on tempfiles
# TODO don't start multiple copies
#
#Legal:
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

from gconf import client_get_default
from gobject import markup_escape_text, timeout_add
from datetime import datetime
from tempfile import NamedTemporaryFile
from base64 import decodestring
from StringIO import StringIO
from ConfigParser import ConfigParser, NoOptionError

import os, Image, gtk

icon = 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtaW4FBABMRDxrZm\
L79/A9HgEsFQCXlZIzKCHLx8IyDgBiCAB8e8P5opS/AwAbIKRAQ7JxnW59AAAAAXRSTlMAQObYZgAA\
AP9JREFUeF6Vk0tyhDAMBWl9/IGZJPc/bcbIEJfLLNIbFq/9hKC8/QdIPSiwNi4SPHQ4UhVnW6MOZh\
ajVjhgqOIPBdyshZ3OusEdYD+O3Z3Vcf+QwA8HWZ0P0lc81znpI5zGXEHkgB9hzPud46GJLQd0Lrhy\
T10eS4AooL8IJIfRoO6XkNDTRKaPwInaBxDwecswHMmqmldr3gU1m8m05zXDvt/vn5CGWEy9kUDs9W\
qxGcPxbHhAsUCt3Dml2u4dagiVsaEVEoJGmpskt1MA0l9BtYsyXYm9FeSeR9kwiNT/stVMqYpqzSF0\
ishlCxkRKGxrAIGKsj1A5qRsT9wX6BfmEQfHqGDl0wAAAABJRU5ErkJggg=='

class PathMissingWarning(UserWarning):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

# Many of the ini files contain ^M (Which we don't like in Linux land)
class WPCConfigParser(ConfigParser):
    def read(self, filename):
        tmpf = StringIO(open(filename).read().replace('\r', '\n'))
        self.readfp(tmpf)

class ConfigException(BaseException): pass

class DeskBling:
    '''A Gnome tray app that displays clock wallpapers. 
    You can get many wallpaper clocks from VladStudio.'''

    __name__ = "DeskBling"
    __author__ = "Jason Gorski (jgorski@gmail.com)"
    __version__, __desc__ = "0.1.1", __doc__

    gccl = client_get_default()
    themeshome = os.path.join(os.path.expanduser("~"), ".wallpaperclock")
    wallhome = None
    THEMEKEY = "/desktop/apps/deskbling/theme"
    conf = {}
    imgbase = None

    def __init__ (self):
        if not os.path.exists(self.themeshome):
            raise ConfigException("Please create %s and place \
unpacked .wcz themes in its subdirectories." % self.themeshome)

        self.wcdconf = { "theme": None }
        try:
            self.loadthemeconf(self.gccl.get_string(self.THEMEKEY))
        except Exception, e:
            self.wcdconf["theme"] = None
            PrefsWindow(self)

    # Only called from wct_NewWindow thingy
    def changetheme(self, cfname):
        # FIXME This is one hacky way of doing things...
        self.gccl.set_string(self.THEMEKEY, cfname)
        self.loadthemeconf(cfname)
        self.update()

    def loadthemeconf(self, theme):
        self.wcdconf["theme"] = theme

        self.wallhome = os.path.join(self.themeshome, self.wcdconf["theme"])

        if not os.path.isdir(self.wallhome):
            PathMissingWarning("Directory %s is missing" % self.wallhome)

        cp = WPCConfigParser()
        cp.read(os.path.join(self.wallhome, "clock.ini"))

        for key in ("hourimages", "refreshhourinterval"):
            self.conf[key] = cp.getint("Settings", key)

        # ampmenabled not always present in ini files
        self.conf["ampmenabled"], self.conf["specampmenabled"] = [False]*2
        for sec, key in (("Settings", "ampmenabled"),
                ("WallpaperClock", "specampmenabled")):
            try:
                self.conf[key] = cp.getboolean(sec, key)
            except NoOptionError:
                pass

        self.conf["havezodiac"] = os.path.isfile(
                os.path.join(self.wallhome, "zodiacLeo.png"))

        for key in ("moonphase", "month", "day", "weekday"):
            self.conf["have%s" % key] = os.path.isfile(
                    os.path.join(self.wallhome, "%s1.png" % key))

        self.imgbase = Image.open(os.path.join(self.wallhome, "bg.jpg"))
        self.lastminute = -90

        self.cache()

    def moon_phase(self, month, day, year):
        # Original by HAB ( http://www.daniweb.com/code/snippet492.html )
        ages = [18, 0, 11, 22, 3, 14, 25, 6, 17, 28, 9, 20, 1, 12, 23, 
                4, 15, 26, 7]
        offsets = [-1, 1, 0, 1, 2, 3, 4, 5, 7, 7, 9, 9]
        
        if day == 31:
            day = 1
        days_into_phase = ((ages[(year + 1) % 19] + (
            (day + offsets[month-1]) % 30) + (year < 1900)) % 30)

        return days_into_phase+1

    def _blank(self):
        image = Image.open(os.path.join(self.wallhome, "bg.jpg"))
        wallname = os.path.join("/tmp", "wallpaper.png")
        image.save(wallname)
        self.gccl.set_bool("/desktop/gnome/background/draw_background", False)
        self.gccl.set_bool("/desktop/gnome/background/draw_background", True)

    def cache(self):
        tspec = {}
        tspec["month"], tspec["day"], tspec["minute"], tspec["year"], \
        tspec["hour"], tspec["weekday"] = [int(x) for x in
                datetime.now().strftime("%m %d %M %Y %H %w").split(" ")]

        self.hourcache = self.imgbase.copy()

        self.lastminute = tspec["minute"]

        if self.conf["havemoonphase"]:
            tspec["moonphase"] = self.moon_phase(tspec["month"], 
                    tspec["day"], tspec["year"])
            assert(type(tspec["moonphase"]) == type(int()))

        if self.conf["havezodiac"]:
            tspec["zodiac"] = self.zodiac(tspec["month"], tspec["day"])

        if tspec["weekday"] == 0 : 
            tspec["weekday"] = 7

        for key in ("moonphase", "month", "day", "weekday", "zodiac"):
            if self.conf["have%s" % key]:
                self._paste(self.hourcache, "%s%s.png" % (key, tspec[key]) )

        # FIXME... sick sick sick...
        if self.conf["hourimages"] == 60:
            self._paste(self.hourcache, "hour%d.png" % int((tspec["hour"])%12*5 + 
                    (5.0*tspec["minute"]/60.0)) )
        elif self.conf["ampmenabled"]:
            if tspec["hour"] > 12:
                self._paste(self.hourcache, "hour%d.png" % (int(tspec["hour"])-12))
                self._paste(self.hourcache, "pm.png")
            else:
                # FIXME... more sickness.... srsly, just think for a second.
                if tspec["hour"] == 0:
                    self._paste(self.hourcache, "hour%d.png" % 12)
                else:
                    self._paste(self.hourcache, "hour%d.png" % int(tspec["hour"]))

                if tspec["hour"] == 12:
                    self._paste(self.hourcache, "pm.png")
                else:
                    self._paste(self.hourcache, "am.png")
        else:
            self._paste(self.hourcache, "hour%s.png" % (tspec["hour"] % 12))

    def _paste(self, base, fname):
        tmpimage = Image.open(os.path.join(self.wallhome, fname))
        base.paste(tmpimage, None, tmpimage) # second tmpimage is mask :(

    def zodiac(self, month, day):
        zlut = [
                (19, "Capricorn"), (18, "Aquarius"), (20, "Pisces"),
                (19, "Aries"), (20, "Taurus"), (20, "Gemini"),
                (22, "Cancer"), (22, "Leo"), (22, "Virgo"),
                (22, "Libra"), (21, "Scorpio"), (21, "Sagittarius")
                ] 

        return zlut[(month-1+(day/zlut[month-1][0]))%12][1]

    def update(self):
        ''' Called by timeout_add, return FALSE to remove repeated call '''

        if self.wallhome == None:
            return

        minute = int(datetime.now().strftime("%M"))

        if minute == 0 or (minute - self.lastminute) >= \
                self.conf["refreshhourinterval"]:
            self.cache()

        image = self.hourcache.copy()

        for key in ("minute",):
            self._paste(image, "minute%s.png" % minute)

        wallname = os.path.join("/tmp", "wallpaper.png")
        image.save(wallname)
        self.gccl.set_string("/desktop/gnome/background/picture_filename", 
                wallname)
        # Following hack not needed on recent versions of Gnome (?)
        self.gccl.set_bool("/desktop/gnome/background/draw_background", False)
        self.gccl.set_bool("/desktop/gnome/background/draw_background", True)

        return True

class StatusIcon(gtk.StatusIcon):
    def __init__(self, wcd):
        # Template came from MetaTracker mailing-list.. Link?
        self.wcd = wcd
        gtk.StatusIcon.__init__(self)
        menu = '''
            <ui>
             <menubar name="Menubar">
              <menu action="Menu">
               <menuitem action="Preferences"/>
               <separator/>
               <menuitem action="Quit"/>
               <separator/>
               <menuitem action="About"/>
              </menu>
             </menubar>
            </ui>
        '''
        actions = [
            ("Menu",  None, "Menu"),
            ("Preferences", gtk.STOCK_PREFERENCES, "_Preferences...", 
                None, "Change WCD preferences", self.on_preferences),
            ("Quit", gtk.STOCK_QUIT, "_Quit...", 
                None, "Quit WCD", self.on_quit),
            ("About", gtk.STOCK_ABOUT, "_About...", None, 
                "About DeskBling", self.on_about)]
        ag = gtk.ActionGroup("Actions")
        ag.add_actions(actions)
        self.manager = gtk.UIManager()
        self.manager.insert_action_group(ag, 0)
        self.manager.add_ui_from_string(menu)
        self.menu = self.manager.get_widget("/Menubar/Menu/About").props.parent
        tmpf = NamedTemporaryFile(mode="w", suffix=".png", dir="/tmp")
        tmpf.write(decodestring(icon))
        tmpf.seek(0)
        self.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file(tmpf.name))
        self.set_tooltip("DeskBling")
        self.set_visible(True)
        self.connect("popup-menu", self.on_popup_menu)

    def on_popup_menu(self, status, button, time):
        self.menu.popup(None, None, None, button, time)

    def on_preferences(self, data):
        PrefsWindow(self.wcd)

    def on_quit(self, data):
        gtk.main_quit()

    def on_about(self, data):
        dialog = gtk.AboutDialog()
        dialog.set_name(self.wcd.__name__)
        dialog.set_version(self.wcd.__version__)
        dialog.set_comments(self.wcd.__desc__.strip("\n"))
        dialog.set_website("http://code.google.com/p/deskbling/")
        dialog.run()
        dialog.destroy()

class PrefsWindow(gtk.Window):
    def __init__(self, wcd):
        gtk.Window.__init__(self)
        self.set_title("DeskBling: Change Theme")
        self.set_default_size(400, 600)
        self.set_geometry_hints(None, min_width=400, min_height=300)
        self.wcd = wcd
        self.set_border_width(10)

        scroller = gtk.ScrolledWindow()

        self.list = ThemeList(self.wcd)
        self.list.connect("cursor-changed", self.on_list_cursor_changed)

        scroller.visible = True
        scroller.can_focus = True
        scroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
        scroller.add(self.list)

        self.add(scroller)
        self.show_all()

    def on_list_cursor_changed(self, treeview):
        pass

class ThemeList(gtk.TreeView):
    def __init__(self, wcd):
        gtk.TreeView.__init__(self)
        self.wcd = wcd

        self.set_headers_visible(False)

        # data in store:
        # 0-icon (preview), 1-info_string, 2-misc_str
        self.store = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
        self.set_model(self.store)

        # get stored data, if it exists
        self.populate()

        if self.size() == -1:
            self.append_emptylistinfo()

        column = gtk.TreeViewColumn()
        renderer_pixbuf = gtk.CellRendererPixbuf()
        column.pack_start(renderer_pixbuf, expand=False)
        column.add_attribute(renderer_pixbuf, "pixbuf", 0)

        renderer_text = gtk.CellRendererText()
        column.pack_start(renderer_text, expand=True)
        column.set_attributes(renderer_text, markup=1)

        self.connect("row-activated", self.on_activated)

        self.append_column(column)

    def on_activated(self, treeview, path, column):
        model = treeview.get_model()
        g_iter = model.get_iter(path)
        hiddencol = model.get_value(g_iter, 2) # Currently full path to theme

        if hiddencol:
            self.wcd.changetheme(os.path.basename(hiddencol))

    def populate(self):
        for wallhome in (os.path.join(self.wcd.themeshome, name) for name in 
                os.listdir(self.wcd.themeshome) if os.path.isdir(
                    os.path.join(self.wcd.themeshome, name))):

            if os.path.exists(os.path.join(wallhome, "minute1.png")):
                self.append(wallhome)

        return True

    def size(self):
        g_iter = self.store.get_iter_first()
        if g_iter == None or self.store.get_value(g_iter, 2) == None:
            return -1
        else:
            return self.store.iter_n_children(None)

    def append(self, dirname):
        pixbuf = self.render_icon(gtk.STOCK_PREFERENCES, 
                gtk.ICON_SIZE_DIALOG, None)

        if not self.size():
            self.store.remove(self.store.get_iter_first())

        for pname in ("preview100x75.jpg", "preview200x150.jpg", "bg.jpg"):
            if os.path.exists(os.path.join(dirname, pname)):
                pixbuf = gtk.gdk.pixbuf_new_from_file_at_scale(
                        os.path.join(dirname, pname), 100, 75, 
                        preserve_aspect_ratio=True)
                break

        cp = EscapedConfigParser()
        cp.read(os.path.join(dirname, "clock.ini"))

        text = "\n".join(( 
            "<b>%s</b> (%s) %sx%s" % (cp.getsetting("name"), 
                os.path.basename(dirname), cp.getsetting("width"), 
                cp.getsetting("height")),
            " by: %s (<i>%s</i>)" % (cp.getsetting("author"), 
                cp.getsetting("email")),
            cp.getsetting("homepageURL"),
            cp.getsetting("description") # <-- usually blank
                ))

        self.store.append([pixbuf, text, dirname])

    def append_emptylistinfo(self):
        pixbuf = self.render_icon(gtk.STOCK_REMOVE, gtk.ICON_SIZE_DIALOG, None)
        self.store.append([pixbuf, "<span size=\"large\"><b>%s</b></span>\n" % \
                "Welcome to DeskBling." + \
            "There are currently no themes in your themes directory.\n\
Uncompress them to ~/.wallpaperclock/ in \n\
<b>separate</b> directories.", None ])

class EscapedConfigParser(WPCConfigParser):
    def getsetting(self, key):
        return markup_escape_text(self.get("Settings", key))

def main():
    wcd = DeskBling()

    StatusIcon(wcd)

    if wcd.wcdconf["theme"]:
        wcd.update()
    #timeout_add(wcd.conf["refreshhourinterval"] * 1000, wcd.update)
    timeout_add(60 * 1000, wcd.update) # Every minute
    gtk.main()

if __name__ == "__main__":
    try:
        main()
    except ConfigException, e:
        print e
